文章概述
本篇文章记录学习Python3的笔记。
参考资料
认识python
python代码
- Python代码不能压缩和混淆;
命令行操作
命令行信息
1 | # 获取命令行参数 |
退出控制台
1 | //windows |
语句换行
使用三引号开头表示多行语句,再次使用三引号结束,如下:
1 | >>> ''' |
python版本信息
1 | $ python --version |
输入输出
1 | //input可以不带参数 |
运行python文件
1 | $ python hello.py |
编程规范
语句写法注意
- 单行语句:不需要加“;”作为结束符;
- 代码块:不需要{}包裹,而是使用四个空格(非tab键)缩进包裹,每个语句块结束都要空出一行;
- 注释:单行注释使用#,多行注释使用三引号;
1 | # 单行注释 |
- 变量命名:以小写字母开头,可以使用驼峰命名或者下划线分割字母的方式命名;
- 函数命名:函数名使用小写字母,单词之间用_下划线分割;
- 语句换行:
1 | # 方式一:末尾加\:换行【不推荐是使用】 |
变量
变量
python是动态语言,定义变量直接对变量赋值,而无需指定变量类型;
1 | y = 10 |
变量命名
变量命名:以小写字母开头,可以使用驼峰命名或者下划线分割字母的方式命名;
变量的类型
- 值类型(不可改变):int、str、tuple
- 引用另类型(引用类型):list、set、dict
变量内存地址
使用id(var)获取变量var的内存地址:
1 | >>> x = 10 |
常量
python中并未严格定义常量的表达式,只是推荐用大写字母来表示常量;
1 | PI = 3.14159265359 |
运算符
算数运算符
1 | +、-、*、/、//(整除)、% |
赋值运算符
1 | =,+=,*=,/=,%=,**=(幂赋值运算符),//= |
关系运算符
比较值是否相等,返回布尔值;
1 | ==,!=,>,<,>=,<= |
逻辑运算
and、or和not来表示计算机的与、或和非运算;
成员运算符
- in:1 in [1,2]=true (注意:字典的成员运算只针对key)
- not in : 与in相反;
身份运算符
返回布尔值
- is :比较的是内存地址是否相等;
- is not:与is相反;
位运算符
位运算符先转换成2进制进行运算结果再转换回来;
1 | &:1&1=1,1&0=0,0&0=0; |
位运算符还可以用于集合之间的运算:
- 求集合交集:{1,2}&{1}={1}
- 求集合合集:{1,2,3,4}|{3,4,7}={1,2,3,4,7}
运算符优先级
运算符优先级从最高到最低如下表:
运算符 | 描述 |
---|---|
** | 指数运算 (最高优先级),如:2**3=8 |
~、+、- | 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@) |
*、/、%、// | 乘,除,取模和取整除 |
+、- | 加法减法 |
>>、<< | 左移、右移位运算符 |
&、 | 按位与 |
^、丨 | 运算符 |
<=、<、>、>= | 比较运算符 |
<>、==、!= | 比较运算符 |
=、%=、/=、//=、-=、+=、*=、**= | 赋值运算符 |
is、is not | 身份运算符 |
in、not in | 成员运算符 |
and、or、not | 逻辑运算符(and的优先级大于or) |
数据类型
数据类型概览
- 基础数据类型有:
- 整数(int):十六进制0x44表示整数更方便,Python的整数没有大小限制;
- 浮点数(float):e代替10,如1.23e9,Python的浮点数没有大小限制,但是超出一定范围就用inf表示无限大;
- 字符串(str);
- 布尔值(bool):True(非0的数值或非空的类型) False(0或空值或None) ,可以使用or、and、not(或与非)运算表达式表示;
- None(NoneType):空值;
- bytes: 字节,用带b前缀的单引号或双引号字符串表示,如:b’abc’;
- complex(复数):python复数单位用j表示,如36j(a+bi(a,b均为实数)的数称为复数,其中a称为实部,b称为虚部,i称为虚数单位);
2. 全部数据类型概览图
思维导图中的数据类型有以下特点:
- 不可变的类型有:数字、字符串、元组;
- 有序的的类型有:字符串、列表、元组,有序的数据类型可以用索引(索引是从0开始)访问、可以对序列进行截取的切片操作;
- 无序的数据类型:集合、字典,没有索引,不可以通过切片操作来截取;
数据类型相关方法
获取数据的类型
1 | >>> type(0); |
判断数据类型
判断数据类型的正确方式:isinstance(var_name, data_type),该方法返回bool值;
1 | >>> isinstance(1,int) |
数据类型相互转换
1 | # str/float->int |
数字
数字包括整数、浮点数、负数、进制数…
进制数
常见进制数
- 二进制:0b作前缀;
- 八进制:0o作前缀;
- 十六进制:0x作前缀;
进制转换方法
- 任意进制数转二进制:bin(任意进制数)
1 | 10->2:bin(10) |
- 任意进制数转十进制:int(任意进制数)
- 任意进制数转八进制:oct(任意进制数)
- 任意进制数转十六进制:hex(任意进制数)
数学运算
保留小数
round(number[,n]):number四舍五入保留n位小数;
1 | //示例: |
布尔类型
type = bool
非零数值、非空字符串、非空list等,就判断为True,否则为False。
- True、False表示布尔值(请注意大小写);
- 布尔值可以用and(与)、or(或)和not(非)运算来表示;
- 布尔值可以用条件运算的结果来表示;
空值
type = None
- 空值用None表示。
- None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
- 判空操作:
1 | //非空判断 |
字符串
type = str
编码
- Python 3的字符串使用Unicode,直接支持多语言。
UTF-8
- UTF-8编码是可变长编码,UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,对比Unicode,更加节省空间;
- 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节
Python文件设置编码
当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:
1 |
|
- 第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
- 第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码
转义字符
常用\表示转义字符,常用方式:
- 换行符:\n
- 横向制表符:\t
- 回车:\r
格式化字符串
格式化字符串的方法:
- 在字符串中使用%占位符,字符串后跟%,%后跟需要被替换的字符串;
- 使用字符串的format方法;
占位符%格式化字符串
- 动态字符串可以使用%占位符的方式格式化字符串,常见占位符如下:
占位符 | 替换内容 |
---|---|
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
- 格式化整数和浮点数还可以指定是否补0和整数与小数的位数:
【示例】
1 | //替换单个字符串 |
format方法格式化字符串
另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……,不过这种方式写起来比%要麻烦得多:
1 | >>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
字符串操作方法
长度
len(str);
len()函数实际上是调用str对象的len()方法来获取字符串的长度,二者等价;
1 | >>> len('ABC') |
乘法复制
字符串乘法(字符串复制):重复n次输出乘号前面的字符串;
1 | // [示例] |
索引取值
如果下标越界会报IndexError
1 | //从左到右: |
索引切片(截取)
字符串按索引截取:str[a:b:step],截取str的a到b索引的元素,不包含b,步长为step;
- str[star,end]切片
1 | //按索引截取 |
- str[star,end,step]切片
1 | "hello world"[0:8:2] # 从索引为0截取到索引为7的位置,步长为2; |
字符索引
获取字符串在另一个字符串中的索引:
1 | >>> 'hello'.index('o'); |
字符串替换
replace方法会替换字符串中所有符合条件的字符串或字符;
1 | >>> 'hello'.replace('lo','*'); |
去除左右空格
1 | >>> ' hello '.strip(); |
字符串转换
1 | //获取字符的整数编码: |
list
type = list
list(列表)是一个可变的有序表,可以添加或删除其中的元素。
列表的定义
列表类似js中的数组,支持任意数据类型的数据组合,支持一维到多维list;
1 | //一维:任意类型的组合,通过下标索引来访问; |
列表的操作
列表的操作与有序数据类型的操作类似,请参考字符串的操作;
CRUD操作
list=[]列表
1 | //添加元素 |
列表长度
1 | len(list) |
数学运算
- 列表的加法:[1,2]+[3]=[1,2,3]
- 列表的乘法:[1,2]*2=[1,2,1,2]
截取
支持类似字符串的冒号(:)切片操作,截取指定索引的列表元素(请参考字符串的操作);
函数生成列表
使用list(range(startIndex,stopIndex,stepLength))表达式生成list:
1 | list(range(1, 22)) |
列表生成式
列表生成式指在[]内使用一行表达式生成一个列表list的表达式;
列表生成式作用的目标可以是列表、集合、元组,最终结果是list;
1.列表生成式作用于list:
1 | # 循环列表生成式 |
2.列表生成式作用在元组:
1 | a = (1, 2, 3) |
元组(tuple)
type = tuple
元组:tuple。tuple和list非常类似,都是Python内置的有序集合,但是tuple不可变,一旦初始化就不能修改,没有添加和删除方法,所以代码更安全,如果可能,能用tuple代替list就尽量用tuple;
注意:tuple如果内含可变元素,可变元素内部可以改变;
元组定义
tuple的元素定义在圆括号()里;
1 | //定义一个元组: |
函数生成元组
使用tuple(range(startIndex,stopIndex,stepLength))方法直接生成一个tuple:
1 | tuple(range(1, 22)) |
序列
有顺序,每个元素都分配一个序号,序列包括:列表、元组、str,他们之间的操作都类似;
序列操作
切片(截取)
支持切片(截取)操作,参考字符串切片操作;
逻辑包含
支持逻辑in、not in运算符:
1 | 2 in [1,2] # True |
序列内置函数
最大值
1 | max(序列) |
最小值
1 | min(序列) |
长度
1 | len(序列) |
join
join函数使用分隔符将序列类型的变量组合成一个字符串;
1 | s = ','.join('hello') |
集合
type = set
集合的定义
- 使用花括号{}包含元素,元素是不可变类型数据,如字符串、数字和元组;
- set是一组元素的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
定义集合变量
- 方式一:直接赋值
1 | >>> x={1,2,3,"hello"} |
- 方式二:通过set([…])定义,需要提供一个list作为输入集合;
1 | >>> x=set({1}) |
空集合
空集合:set(),注意{}不能表示空集合,type({})=dict(字典)
1 | >>> type(set()) |
集合的操作
添加
通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果;
1 | >>> x = {1} |
删除
通过remove(key)方法可以删除元素;
1 | >>> s = {1,2,3} |
集合的运算
- 支持逻辑包含:in、not int;
- 支持求集合差集:{1,2,3}-{1}={2,3}
- 支持求集合交集:{1,2}&{1}={1}
- 支持求集合合集:{1,2,3,4}|{3,4,7}={1,2,3,4,7}
字典
type = dict
Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
特点
- 查找和插入的速度极快,不会随着key的增加而变慢;
- 需要占用大量的内存,内存浪费多。
- 无序的(不支持索引)、不支持切片操作、不重复。
字典的定义
字典的定义类似java的map,用花括号包裹许多个key:value,用逗号分隔;
语法格式:{key1:value1,key2:value2,…}
注意:key是不可变类型,value可以是任意类型;
空字典
空的字典:type({})=dict
示例
- 例1:
1 | >>> d = {1:'one','two':2} |
- 例2:
1 | >>> x = {1:2,'1':1,(1,3):1,3:{1,3},6:{'one':'hello'}} |
key的特点
- key不能相同,相同则会覆盖对用的value值;
- key中数字和字符串被认为不同的key,如1,“key”;
1 | >>> x = {1:2,'1':1} |
- key的数据类型:是不可变的类型,包括:int,str,元组;
1 | >>> x = {1:2,'1':1,(1,3):1} |
字典的操作
添加
- 直接通过d[key]=value的方式赋值;
1 | >>> d = {} |
- 通过内置函数setdefault(key,defaultValue):如果键不存在于字典中,将会添加键并将值设为defaultValue;
1 | >>> d={} |
根据key获取值
- d[key]: 中括号索引的方式,如果key不存在,dict就会报错;
1 | >>> d = {1:'one'} |
- d.get(key)的方式:如果key不存在,可以返回None;
1 | >>> d = {1:'one'} |
- d.get(key,default)的方式:如果key不存在返回default,但不会新增到字典中;
1 | >>> d = {} |
删除key
要删除一个key,用pop(key)方法,对应的value也会从dict中删除:
1 | >>> d = {1:'one'} |
遍历字典
语法:
1 | # 直接遍历字典,默认遍历结果是dict的key |
示例:
1 | d = {1: 'one', 2: 'two'} |
数据解构赋值
序列解包类似js里的解构赋值,但是解构左右两边数量必须对等;
生成器
一边循环一边计算,根据前面的元素推算后面的元素的机制,称为生成器generator,generator也是可迭代对象;
表达式生成器
构建一个generator只需要将列表生成式的[]换成()即可得到generator对象;
1 | L = [x * x for x in range(3)] |
函数生成器
- 如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator;
- yield关键字:在每次调用next()的时候,执行函数遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
定义示例
以实现一个斐波那契数列函数为例来实现一个generator:
1 | # 原实现斐波那契数列的函数实现 |
函数返回值
如果定义函数生成器最终return返回一个值,如果需要获得返回值的话,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
1 | def fib(max): |
生成器迭代
使用next(g)可以遍历generator,但是不知道临界,所以一般都使用循环来迭代generator(不需要关心StopIteration的错误):
1 | # 循环来遍历生成器 |
迭代器
Iterable
可以直接作用于for循环的对象统称为可迭代对象:Iterable
可迭代的数据类型有:
- 一类是集合数据类型,如list、tuple、dict、set、str等;
- 一类是generator,包括生成器和带yield的generator function。
Iterable对象判断
可以使用isinstance(aim,Iterable)内置函数来判断是否是Iterable对象:
1 | >>> from collections import Iterable |
Iterator
- 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
- Python的for循环本质上就是通过不断调用next()函数实现的;
Iterator对象判断
可以使用isinstance()判断一个对象是否是Iterator对象:
1 | >>> from collections import Iterator |
Iterable转Iterator
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
1 | >>> isinstance(iter([]), Iterator) |
Iterator数据流对象
- Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
- Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
条件控制语句
条件判断
if条件判断语句,判断表达式必须,返回结果可以使用pass占位,pass是空语句/占位语句;
语法
1 | if <条件判断1>: |
1 | # if |
示例
1 | # eg1: |
三元表达式
python中的三元表达式的效果类似java中的三目运算符(expression?r2:r2);
- 表达式格式:结果为真的返回 if 表达式 else 结果为假的返回
- 示例:
1 | # python中的三元表达式 |
循环
while循环
语法
当while表达式满足会一直循环执行while语句块中的逻辑,当不满足时跳出循环,或者执行else语句块;
1 | # while |
示例
1 | # eg1 |
for循环
- for循环的目标是可迭代对象类型(Iterable)的数据,就可以使用for循环进行迭代。
- 判断一个对象是否是可迭代对象,通过collections模块的Iterable类型判断;
1 | >>> from collections import Iterable |
for…in…循环
1 | # for...in...循环遍历列表 |
1 | # 嵌套for...in...循环遍历列表; |
for…in…else循环
1 | # for...in...else循环结束后执行else; |
for…in range循环
for…in range(startIndex,endIndex,step)循环按索引访问数据,不包含endIndex;
1 | # 以步长1,遍历列表: |
for循环获取索引
Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
- eg1: 遍历列表:
1 | >>> for i, value in enumerate(['A', 'B', 'C']): |
- eg2: 遍历由元组组成的列表:
1 | for i, (x, y) in enumerate([(1, 1), (2, 4), (3, 9)]): |
循环控制语句
break
break 仅终止当前循环,终止当前循环后不会执行当前循环的else,如果外层还有循环,则继续执行外层的循环;
1 | arrays=[1,3,4] |
continue
continue:结束当前循环,继续执行下个循环;
1 | arrays=[1,3,4] |
Python实现java语言switch特性
Python中没有类似java语言的switch API,可以用elif或者字典来代替;
字典switch
这里主要是dict字典来实现switch语法;
- key对应简单的值:
1 | # 简单的switch,利用字典的key实现 |
- key对应函数:
1 | def getOne(*args): |
函数
内置函数
python内置了许多函数,可以直接调用,常用的内置函数有:
- abs(x): 计算x的绝对值;
- max(a,b,…): 获取多个参数的最大值;
函数命名
函数命名:函数名使用小写字母,单词之间使用下划线(_)分割;
定义函数
- 定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。
- 如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。
- return None也可以简写为return。
py文件定义函数
- 定义一个空函数:
1 | def funcname(parameter_list): |
- 定义一个求绝对值的函数;
1 | def my_abs(x): |
python交互环境定义函数
在Python交互环境中定义函数时,注意Python会出现…的提示。函数定义结束后需要按两次回车重新回到>>>提示符下:
1 | >>> def my_abs(x): |
函数返回多个值
函数返回多个值时,python将多个值放到一个tuple中,语法上可以省略():
1 | def multi_value(): |
函数的参数
默认参数
定义函数时,函数的参数可以设置默认值,设置默认值使用等号直接赋值即可:
1 | def myfn(name, sex='man'): |
定义默认参数要牢记一点:默认参数必须指向不变对象!
可变参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple.
定义函数时接收的参数个数不确定时,可以在参数前加*,此时参数就是可变参数;
在调用接收可变参数的函数时,如果传入已有的list或tuple作为多个参数时,需要在调用处给list或tuple前也加*,这样list或tuple的元素才能正确转化为一个元组,否则转化后的元组只有整个list或整个tuple作为1个参数;
1 | def myfn(*args): |
关键字参数
关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict.
关键字参数使用 **kw表示;
在调用接收关键字参数的函数时,如果传入已有的dict作为多个参数时,需要在调用处给dict前也加**,这样dict的元素才能正确转化为一个dict,否则转化后的dict只有整个dict作为1个参数;
1 | def myfn(**kw): |
命名关键字参数
命名关键字参数主要是为了限制调用者传入哪些参数名的参数,如果传入了函数未声明接收的参数名则会报错,是非必要参数;
注意点
1 | - 关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。 |
示例
1 | def myfn(name, *, city, age=23): |
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。
参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
要点1
2- 虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
- 对于任意函数,都可以通过类似func(*args, **kw)的形式定义。
示例
1 | def f1(a, b, c='None', *args, name, **kw): |
函数的要点
- 函数可以赋值给变量:f=fn()->f();
- 函数可以作为参数:fn(fn1());
- 函数内可以再定义函数,函数可以作为函数的返回;
- 函数内的变量和模块的变量同名时,就近原则,优先使用函数内定义的变量;
- 函数内给模块变量赋值:python就会看做这是重新定义一个与模块变量同名的局部变量;
- 函数内给模块变量赋值:需要在函数内使用global关键字重新声明一下模块变量为全局变量;
1 | # 函数中的函数调用 |
递归函数
如果一个函数在内部调用自身本身,这个函数就是递归函数。
1 | def fact(n): |
递归栈溢出
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000);
解决递归调用栈溢出的方法是通过尾递归优化;
尾递归优化
尾递归是指,对函数返回的优化,即:函数返回时调用函数自身,并且,return语句不能包含调用函数自身之外的表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
严重警告:Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
函数式编程
纯粹的函数式编程语言编写的函数没有变量;
Python的函数允许使用变量,因此,Python不是纯函数式编程语言。
函数式编程特点
- 允许使用变量;
- 允许把函数本身作为参数传入另一个函数;
- 函数允许返回一个函数;
- 函数内可以定义函数;
- 函数名可以赋值给变量,此时不会立刻执行函数,使用变量名加圆括号f()调用;
闭包
当函数返回一个函数名时,函数名对应的函数并没有立刻执行,而是直到调用了f()才执行,这称为闭包(closure).
闭包的特点
- 闭包返回的函数并未执行,返回函数中不要引用任何可能会变化的变量;
- 闭包返回的函数每次调用都会返回一个新的函数,并且每次调用互不影响;
- 闭包返回的函数内,通过nonlocal关键字声明变量不是局部变量,而是父函数的变量;
示例
- eg1: 一个简单的闭包
1 | def count(): |
- eg3:闭包返回的函数内的非局部变量:
1 | origin = 0 |
闭包函数的闭包信息
- 打印f函数的闭包信息
1 | print(f.__closure__) |
- 打印f函数闭包内环境变量(环境变量按字母排序对应索引)的值
1 | print(f.__closure__[0].cell_contents) |
闭包的缺点
- 闭包根函数的变量(环境变量)常驻内存,容易发生内存泄漏;
匿名函数
在Python中,对匿名函数提供了有限支持,匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
语法
关键字lambda用来修饰匿名函数,使用冒号分割参数和返回表达式,冒号前面的x表示函数参数,冒号后面是返回结果。
1 | # 匿名函数示例 |
装饰器
在代码运行期间动态增加函数的功能而且不会改变原有函数的定义的方式,称之为装饰器(Decorator)。
装饰器:可以不改变函数逻辑,给函数新增功能;
装饰器函数
- 定义装饰器函数:
定义一个装饰器函数,需要接收一个被装饰函数为参数,装饰器内部定义一个返回被装饰函数的扩展功能函数,最终装饰器函数返回扩展功能函数 (需要注意:扩展函数应该加@functools.wraps(func)修饰,确保最终返回的函数保持被装饰函数本身的属性)。 - 使用装饰器函数:
当我们给被装饰函数上方添加声明:@装饰器函数名,此时直接调用被装饰函数时就得到了扩展的功能,实际是一种装饰模式(AOP编程思想);
示例
- 定义一个打印日志的装饰器函数:
1 | import functools |
装饰器函数带参数
如果装饰器本身需要传入参数,定义最外层装饰器函数接收参数,内部定义一个无参数的装饰器函数,最外层有参数的装饰器函数返回无参数的装饰器函数名即可;
1 | import functools |
装饰器通用优化
- 装饰器内给被装饰函数支持可变参数:func(*args);
- 装饰器内给被装饰函数支持可变参数和可变关键字参数:func(args,*kw);
偏函数
Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。
创建偏函数
1 | 1> 创建偏函: functools.partial就是帮助我们创建一个偏函数的; |
示例
- 创建一个偏函数:用于求2进制数对应10进制:
1 | import functools |
应用场景
当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数(偏函数),这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
内建的高阶函数
变量可以指向函数,函数的参数能接收变量,函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。
python提供了许多内建的高阶函数供开发者调用;
map
map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
语法
1> 语法格式:
1 | map(fn,iterableData) |
2> 参数说明:
- fn:函数;
- iterableData:iterable类型的数据;
示例
- 对序列中的每个元素求平方:
1 | def f(x): |
- 将序列的元素都转str:
1 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
reduce
reduce函数来自functools模块;
reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,返回一个计算结果,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
语法
1 | reduce(function, sequence, initial=None) |
示例
- reduce累加序列中元素的实现:
1 | from functools import reduce |
- 把序列[1, 3, 5, 7, 9]变换成整数13579:
1 | from functools import reduce |
filter
Python内建的filter()函数用于过滤序列,filter()函数返回的是一个Iterator;
和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
示例
- 在一个list中,删掉偶数,只保留奇数:
1 | def is_odd(n): |
sorted
- Python内置的sorted()函数就可以直接对list进行排序;
1 | >>> sorted([36, 5, -12, 9, -21]) |
- sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序;
语法
1 | sorted(list, func, reverse): |
示例
- 对list元素求绝对值后再排序:
1 | >>> sorted([36, 5, -12, 9, -21], key=abs, reverse=false) |
注意
- 默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。
模块
模块的概念
在Python中,一个.py文件就称之为一个模块(Module)。
- 模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用。
- 模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中。
模块的命名
- 模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
- 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块。
包
为了避免模块名冲突,Python引入了按目录来组织模块的方法,称为包(Package)。
包的定义
1 | 如果目录文件夹中有一个__init__.py文件,Python就把这个目录当成包,否则Python就把这个目录当成普通目录。 |
要点
1 | 1> __init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,它的模块名就是包名。 |
示例
- mypackage多级层次的包结构:
1 | mypackage |
导入模块
Python模块文件导入另一个模块后,当前模块就可以通过(包名.)模块名或者别名来访问模块中的所有功能了。
导入模块注意点
- 当python中的模块1导入了模块2,则模块2里的代码会被执行一遍;
- 要避免循环导入:如module1引用module2,module2又引用module1;
语法
导入其它模块的语法有两种方式:
- 方式一:import…
- 方式二:from…import…
import…
1 | import package_name.module_name as alias |
from…import…
- 注意点:
1 | 【注意】 |
- 语法格式:
1 | from [包名.]模块 import 函数/变量 |
模块导入示例
导入内建模块
以导入sys模块为例:
1 | import sys |
导入自定义模块
1> 创建utils包,在保重创建一个test模块:
1 | desc = 'from test module' |
2> 如果创建一个与test模块不同包的模块main,来使用test模块的功能:
1 | import utils.test |
如果导入的模块包级别特别深时,可以使用别名:
1 | import utils.test as test |
3> 如果创建一个与test模块同包的模块main,来使用test模块的功能:
1 | import test |
作用域
作用域有公开的和私有的作用域,来控制模块内的属性是否能被外界访问;
公开作用域
- 模块内正常的函数和变量名是公开的(public),可以被直接引用;
- 模块内类似xxx这样的变量是特殊变量,可以被直接引用,但是有特殊用途,我们自己的变量一般不要用这种变量名;
1 | 【特殊变量】 |
私有作用域
在Python中,是通过下滑线(“_”)前缀来实现私有作用域(private),被私有化的属性不能被外部访问;
1 | private函数或变量不应该被别人引用,只需给名称加单下划线("_")前缀即可; |
安装第三方模块
pip包管理工具
在Python中,安装第三方模块,是通过包管理工具pip完成的。
1 | 【pip的安装】 |
第三方库的获取
- 官方地址:
一般第三方库都会在Python官方的pypi.python.org网站注册,可以直接在此网站搜索需要的库; - anaconda官网 :一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,Anaconda会把系统Path中的python指向自己自带的Python,可直接使用python命令进入。
安装第三方库
- 安装Pillow库,参考官方文档,安装命令如下:
1 | $ pip install Pillow |
模块搜索路径Path
当使用import加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错ImportError。
默认搜索路径
默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中;
1 | 【查看搜索路径】 |
自定义模块搜索路径
添加自己的搜索目录,有两种方法:
1> 方法一:是直接修改sys.path,添加要搜索的目录(这种方法是在运行时修改,运行结束后失效)。
1 | >>> import sys |
2> 方法二:是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。
面向对象编程
- 面向对象的设计思想是抽象出类(Class),根据Class创建实例(Instance)对象;
- 面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。
类和实例
类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
创建类
语法格式
定义类的语法格式如下:
1 | class ClassName(object): |
【注释】
- ClassName:类名通常是大写开头的单词,不同于变量名、函数名,类名推荐驼峰命名;
- (object):表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
示例
1 | # 创建类 |
类实例
定义好了类,就可以通过ClassName()创建类的实例对象,即创建实例是通过类名+()实现的;
- 类的实例可以自由的访问类公开的成员变量和方法;
- 可以自由地给一个类的实例变量绑定属性, 此时只作用于此实例,不会影响到其它实例;
示例
1 | # 创建类 |
类的构造函数
类可以起到模板的作用,可以定义类的构造函数(比普通函数多了个默认不用传的self参数,代表类自身的实例),让类实例化时传入一些默认的属性值,作为类初始化属性的值。
构造函数的默认参数self可以直接给当前类添加新属性
构造函数语法
1 | 通过定义一个特殊的__init__方法,在创建实例的时候,就把设定的必须的属性绑上去; |
例如:在创建Student类实例的时候,传入name、score属性
1 | class Student(object): |
类变量
概述:类变量必须通过“类名.变量名”来访问和赋值;
- 获取类的所有变量组成的字典:通过类名.__dict__来访问,返回类所有成员组成的字典;
- 类方法访问类变量:
1 | 1> 类名.变量名 |
1 | class Student(object): |
实例变量
概述:实例变量必须通过“实例对象.变量名”来访问和赋值;
- 实例变量的定义:必须在类中用self.变量名定义,才会被添加到实例变量字典中;
- 实例方法中使用实例变量:最好加self.变量名;
- 实例变量和类变量的访问:首先会在实例变量里找,没有用self.varname定义时,python会到类中再查找,返回同名的类变量,如果类中也没有,会到父类中继续查找;
- 实例变量字典:通过对象.__dict__来访问,返回字典;
实例方法
实例方法相对于函数,增加了一个默认参数self,可以直接操作self给当前类添加新属性;
定义类实例的方法
实例方法实际就是函数添加了self作为第一个参数,调用时除了self不用传递,其他参数正常传入;
1 | class Student(object): |
类名访问实例方法
1 | class Student(object): |
实例方法访问类变量
1 | 1. 方法内部直接使用:类名.变量名; |
1 | class Test(object): |
实例方法访问实例变量
1 | self.变量名 |
类方法
类方法定义时,也需要用一个标识符cls作为第一个参数(cls同self类似也可以是任意的字符),cls代表类本身,但是方法需要用装饰器@classmethod来声明;
1 | class Test(object): |
要点
- 在python中类的对象访问类方法是可以的,不推荐使用;
- 类方法可以访问静态方法;
- 类方法不能直接访问实例方法;
类的静态方法
概述:静态方法可以同时被类和对象访问,不同于类方法和实例方法的是不需要默认有第一个参数代表静态方法,但是需要装饰器@staticmethod作为静态方法的标识;
1 | class Test(object): |
要点
- 静态方法可以用类方法代替使用;
- 静态方法不可以直接访问实例方法;
- 静态方法可以访问类方法:类名.方法名();
类的访问限制
私有变量
私有变量
如果要让内部属性不被外部访问,可以把属性的名称前加上双下划线,在Python中,实例的变量名如果以开头,就变成了一个私有变量(private)。
要点
- 私有变量只有类的内部可以访问,外部实例不能访问。
- 可以通过对外暴露返回私有变量的方法,供外部调用来访问私有成员;
- 注意:同时以双下划线做前缀和后缀的变量是特殊变量,类的外部实例可以直接访问。
示例:
1 | class Student(object): |
单下划线前缀属性
通常模块的私有成员使用单下划线做前缀,类似的,在类中,以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
私有方法
同私有变量
访问私有属性
强烈建议不要这么干
Python私有属性不是绝对的,可以通过如下格式访问:
1 | 实例名._类名__方法名()或属性名 |
1 | # 示例 |
继承
- Python的类也支持类似java中的继承,在定义类时,类名后面紧跟的圆括号里的类名就是当前定义的类需要继承的父类,通常无需继承时,默认要写object表示所有类的父类;
- Python允许使用多重继承,继承多个类时,用逗号分隔即可;
单继承示例
1 | # 示例1 |
多继承示例
1 | # 示例2 |
多态
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说,多态就是用基类的引用指向子类的对象。
例如:当子类和父类都存在相同方法时,子类的方法会覆盖了父类的同名方法。
1 | class Animal(object): |
对象的类型判断
判断对象的类型,同判断变量类型类似,两种方法:
- type(x)
- isinstance(x,type)
type()
- 基本的type()方法可以判断一些基本的数据类型,也可以判断对象的类型:
1 | class Animal(object): |
- 判断一个对象是否是函数,可以使用types模块:
1 | import types |
isinstance()
建议一直使用来判断对象或者变量的数据类型;
- 对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数;
- 能用type()判断的基本类型也可以用isinstance()判断;
1 | class Animal(object): |
获取对象的信息
获取所有属性和方法
Python提供内置函数dir()来获取对象的所有属性和方法,dir()返回一个包含字符串的list;
1 | class Animal(object): |
属性变量的设置和获取
- 获取对象的属性:getattr
1 | # 如果不存在,会抛异常,推荐设置默认值 |
- 设置对象的属性:setattr
1 | setattr(obj,str,default) |
示例:
1 | class Animal(object): |
判断是否包含某属性
hasattr()内置函数用来判断某对象是否包含某个属性;
1 | class Animal(object): |
实例绑定属性和类绑定属性
- 实例属性属于各个实例所有,实例之间互不干扰;
- 类属性属于类所有,所有实例共享一个属性;
- 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。
实例绑定属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
绑定方式
- 给实例绑定属性的方法是通过实例变量,或者通过self变量;
1 | class Student(object): |
- 通过setattr内置函数设置实例的属性;
限制允许绑定的属性
1 | 1> Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性; |
1 | # 示例 |
类绑定属性
- 直接在class中定义属性变量,这种属性变量是类属性,可以直接通过类名.属性变量名访问;
- 当我们定义了一个类属性后,这个属性虽然归类所有,类的所有实例都可以访问到。
1 | class Test(object): |
实例绑定方法和类绑定方法
实例绑定方法
类的实例的操作,不影响类的其它实例;
直接给一个初始化的实例绑定的方法,对另一个实例是不起作用,可以使用types模块的MethodType函数给实例对象绑定方法;
1 | from types import MethodType |
类绑定方法
可以定义以个函数,然后通过类名.方法名直接给类绑定一个方法;
1 | class Student(object): |
类方法的装饰器
装饰器(decorator)可以给函数动态加上功能,对于类的方法,装饰器一样起作用。
@property属性读写限制和检查
Python内置的@property装饰器就是负责把一个方法变成属性调用的, 并且可以限制属性的读写;
- 在方法上添加@property,负责把此属性的getter方法变成属性的读取,此时只读。
- @属性名.setter,负责把一个setter方法变成属性赋值;
1 | class Student(object): |
类可定制
Python为类class内部提供了一些特殊方法用来定制类,允许重写这些特殊方法,为类的实例添加一些特殊的功能。
__str__
字符串输出实例
1 | 1> __str__用来输出类实例字符串信息,通常我们可以重新定义它来返回一个规则的字符串; |
示例
1 | class Student(object): |
__iter__
类添支持迭代
1 | 如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法, |
示例:以斐波那契数列为例,写一个Fib类,可以作用于for循环:
1 | class Fib(object): |
__getitem__
类实例支持索引访问
__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice
- 当接收的是int参数时,仅支持下标访问:
1 | class Fib(object): |
- 支持切片访问:接收的参数也可以是切片对象,需要判断做处理;
1 | class Fib(object): |
拓展
1 | 1> 与__getitem__对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。 |
通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
__getattr__
类实例调用未定义的属性和方法处理
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。
要点
1 | 1> 重写__getattr__()方法,可以对未知属性和方法进行处理; |
示例
1 | class Student(object): |
__call__
可调用对象
要点
1 | 1> 任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用; |
示例
1 | class Student(object): |
可调用对象
通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
1 | >>> callable(Student()) |
枚举类
Python提供了Enum枚举类来实现每个类中的常量都是class的一个唯一实例;
概念要点
概念
- 枚举类本质还是一个类,需要继承enum模块的Enum类或IntEnum类,枚举成员名称大写;
1 | from enum import Enum,IntEnum,unique(标识枚举标签的值不能相等) |
枚举值类型
- Enum类型的枚举成员的值可以是字符串、数字类型;
- IntEnum类型的枚举成员的值只能是int类型;
使用注意
- Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。
- 枚举成员是不可更改的常量;
- 既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量;
- 枚举不能实例化,它通过单例模式设计的;
- 枚举有两个成员值相等时,第二个重复的是第一个的别名,一般别名枚举标签不会被打印,获取到的都是第一次出现的标签名;
- 如果需要打印别名可以遍历:枚举类.__members__(只打印枚举标签的名称),枚举类.__member__.items()(打印的是各个枚举成员的所有内容元组);
简单的枚举类
1 | from enum import Enum |
自定义枚举类
如果需要更精确地控制枚举类型,可以从Enum派生出自定义类;
自定义枚举类,需要使用@unique装饰器帮助我们检查保证没有重复值。
1 | from enum import Enum, unique |
访问枚举类
1 | 1. 获取枚举的标签名称:枚举类名.标签名.name |
1 | from enum import Enum, unique |
动态创建类
使用type函数
通过type()函数创建的类和直接写class是完全一样的;
type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)…的定义;
语法
1 | type('类名',(object,父类元组...),属性字典) |
要创建一个class对象,type()函数依次传入以下3个参数:
1 | 1. class的名称; |
示例
1 | # 先定义函数 |
使用metaclass
除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass(元类)。
定义和使用元类的步骤:
- 元类需要继承type类,并重写__new__()方法用于创建类实例;
- 元类需要重写的__new__()方法实际是增加对attrs逻辑处理,最终回调type.__new__(mcs, name, bases, attrs),来应用修改后的实例化逻辑;
- 自定义一个类来使用元类时,只需要给自定类的父类元组中添加metaclass属性来指向已定义的元类,那么自定义类实例化时,就会调用元类定义的__new__()方法来进行实例化;
应用metaclass的类的mataclass特性会继续应用到子类;
__new__()
__new__(mcs, name, bases, attrs)方法接收到的参数依次是:
- mcs: 当前准备创建的元类模板自己的实例对象;
- name: 类的名字;
- bases: 类继承的父类集合;
- attrs: 类的属性和方法集合。
示例
- metaclass给自定义的MyList增加一个add方法来拓展list:
不要使用元类来添加方法,此处只做演示;
1 | # metaclass是类的模板,所以必须从`type`类型派生: |
- metaclass可以隐式地继承到子类:
1 | # metaclass是类的模板,所以必须从`type`类型派生: |
元类实现ORM
ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
实现ORM简单框架的代码加注释,详细如下:
1 | # Field: 存储数据表的映射字段名和类型 |
抽象类
定义抽象类
定义一个类,并指定抽象类的metaclass=ABCMeta(Abstract Base Classes)即表示抽象类;
- 抽象类不能直接被实例化;
- 抽象类中使用@abstractmethod装饰器标识的方法时抽象方法,方法体使用pass无需实现,但是子类必须实现抽象方法;
- 可以直接使用继承抽象类的方式来直接使用抽象类;
- 抽象类中使用@property+@abstractmethod装饰的方法称为抽象属性;
示例
1 | from abc import ABCMeta, abstractmethod |
错误处理
- Python所有的错误都是从BaseException类派生的;
- 只有在必要的时候再使用错误类型,尽量使用Python内置的错误类型。
捕获和处理错误
Python异常处理使用try…except…else…finally,try语句块内是需要被检查的代码, except捕获的异常类型可以有多个,else是没有异常时的处理,finally每次都会执行;
1 | import logging |
错误调试
断言错误
断言使用assert标识的语句,assert后跟表达式和错误信息,如果表达式返回True说明断言成功,否则就是断言失败,断言失败标识后面的语句就会报错;
注意:断言失败后会抛出异常,中断程序,不推荐;
1 | def foo(s): |
关闭断言
启动Python解释器时可以用-O参数来关闭assert,关闭后,你可以把所有的assert语句当成pass来看。
1 | $ python -O err.py |
打印错误日志
- 使用logging模块,来打印错误日志,好处是不会抛出错误中断程序;
- logging模块有debug,info,warning,error等几个级别的信息;
1 | # 导入logging模块 |
pdb命令调试
略
IDE调试
PyCharm和VScode都支持很智能的调试功能;
参考
正则表达式
概述
正则表达式由普通字符和元字符组成,普通字符就是普通的字符,元字符是正则规定的特殊字符,Python提供re模块来操作正则表达式的模块;
正则元字符
转义字符
正则表达式使用斜杠(\)来表示转义字符;
边界符
元字符 | 描述 |
---|---|
^ | 匹配正则表达式的开始 |
$ | 匹配正则表达式的结束 |
数量限制符
元字符 | 描述 |
---|---|
* | 匹配此元字符前面的子表达式任意次 |
+ | 匹配此元字符前面的子表达式一次或多次 |
? | 匹配此元字符前面的子表达式零次或一次,也可做非贪婪匹配限制符 |
{n} | 匹配n(n>=0)次 |
{n,} | 至少匹配n(n>=0)次 |
{n,m} | 最少匹配n次,最多匹配m次(n<=m,n>=0,m>=0) |
匹配表达式
1 | . 匹配除“\r\n”之外的任何单个字符,要匹配包括“\r\n”在内的任何字符,请使用像“[\s\S]” |
获取和非获取匹配
1 | | 元字符 | 描述 | |
其它匹配字符
元字符 | 描述 |
---|---|
\f | 匹配一个换页符。等价于\x0c和\cL |
\n | 匹配一个换行符。等价于\x0a和\cJ |
\r | 匹配一个回车符。等价于\x0d和\cM |
\t | 匹配一个制表符。等价于\x09和\cI |
\v | 匹配一个垂直制表符。等价于\x0b和\cK |
\xn | 匹配十六进制数n的转义值(例如:“\x41”匹配“A”) |
转义字符问题
由于Python的字符串本身也用\转义,因此强烈建议使用Python的r前缀来写正则,就不用考虑转义的问题了:
1 | s = 'ABC\\-001' # 'ABC\-001' |
re模块
Python提供re模块,提供所有与正则表达式相关的功能;
正则匹配方法
查找和匹配
match、search、findall
1.语法:
1 | re.match(pattern,string,flag):判断字符串的首个元素是否匹配pattern,不匹配直接返回None,匹配时返回一个结果对象; |
2.示例:
1> match
1 | import re |
2> findall
1 | import re |
分组匹配
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。正则表达式中用()包裹的部分表示的就是要提取的分组(Group)。
要点
- 如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。
- group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。
示例
- 普通分组和子串的提取:
1 | >>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345') |
- 正则来识别合法的时间:
1 | >>> t = '19:05:30' |
贪婪匹配
贪婪匹配模式:正则表达式默认匹配模式,尽可能多的匹配所搜索的字符串;
1 | >>> re.match(r'^(\d+)(0*)$', '102300').groups() |
非贪婪匹配
非贪婪匹配模式:尽可能少的匹配所搜索的字符串;
非贪婪模式限制符为问号?,即当?字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。
1 | 例如:对于字符串“oooo”的贪婪和非贪婪匹配处理? |
1 | # 示例 |
re模块其他操作函数
查找替换
- 语法:
1 | re.sub(pattern,replace,string,count) |
- 示例:
1 | import re |
预编译该正则表达式
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:
编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。
1 | >>> import re |
正则切分字符串
用正则表达式切分字符串比用固定的字符更灵活;
1 | # 普通分隔符切分,无法识别连续空格 |
正则表达式收集
校验数字的表达式
1 | 1. 数字:^[0-9]*$ |
校验字符的表达式
1 | 1. 汉字:^[\u4e00-\u9fa5]{0,}$ |
Email地址
1 | ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
域名
1 | [a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.? |
手机号码
1 | ^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$ |
IP地址
1 | # 提取IP地址 |
IO编程
文件读写
Python中文件读写的IO操作主要有以下步骤内容:
- 通过with open()函数打开文件,with的作用是自动关闭文件(等价于调用close函数关闭文件);
- 使用read或write进行读写操作;
- 读写文件的过程,最好捕获错误;
读文件示例
读一个文本文件,输出所有内容:
1 | try: |
等价于:
1 | f = None |
读写文件的内置函数
open()
open函数用来打开文件准备进行操作,语法格式如下:
1 | open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True) |
示例
1 | # 打开读一个二进制文件 |
read()
读文件,返回字符串,语法解释如下:
1 | read(size): size不传时默认读取全部,否则按size大小读; |
readlines()
调用readlines()一次读取所有内容并按行返回list。
1 | for line in f.readlines(): |
类似方法:readline()可以每次读取一行内容,
write()
写文件
1 | write(str): str为写入的内容; |
内存读写
StringIO
StringIO作用是在内存中读写str。
内存读写操作
- write(str)方法将数据写入内存,调用此方法后读的位置会变成当前字符末尾,如果调用read()读的话,需要先调用seek(0)将读的位置重置才行;
- getvalue()方法用于获得写入后的str;
1 | from io import StringIO |
- StringIO实例对象可以像读文件一调用read方法读,但是如果实例化后再调用写入方法则需要调用seek(0)重置读的位置;
1 | # 实例化后读 |
1 | # 写入后再读,需要seek(0) |
ByteIO
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
读写操作
1 | from io import BytesIO |
操作系统和文件目录
Python的os模块封装了操作系统的目录和文件操作,要注意这些函数有的在os模块中,有的在os.path模块中。
获取操作系统信息
1 | import os |
操作文件和目录
1 | import os |
复制文件
- os模块中没有复制文件的函数,但是读写文件可以完成文件复制,也可以使用shutil模块提供的copyfile()
- shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
序列化
pickling序列化
概念
Python提供了pickle模块来实现序列化。
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
- 序列化:序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
- 反序列化:反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
序列化操作
- pickle.dumps()方法把任意对象序列化成一个bytes;
- pickle.loads()方法把序列化的bytes反序列化为Python对象;
- pickle.dump()方法将一个对象序列化,并写入文件;
- pickle.loads()方法从文件中读取byte并反序列化出对象;
1 | import pickle |
JSON序列化
json写法
注意:json对象的key和字符串value必须用双引号包裹;
1 | json对象写法:{“key”:"value",....} |
json和python的数据类型
json和python的数据类型对应关系如下:
json | python |
---|---|
object | dict |
array | list |
string | str |
number | int或float |
true | True |
false | False |
null | None |
处理json
json字符串转python对象
json.loads()函数是将json格式数据转换为字典
1 | import json |
python对象转json字符串
json.dumps()函数是将一个Python数据类型列表进行json格式的编码;
1 | import json |
处理中文
json.dumps接收ensure_ascii参数表示是否使用ascii编码,默认为True时不能正确显示中文,如果显示中文请设置为False;
1 | obj = dict(name='小明', age=20) |
读写json文件
- json.dump()函数将json信息写入.json文件;
- json.load()函数从.json文件中读取json信息;
1 | import json |
json序列化类对象
1 | import json |
多任务
多任务的实现有3种方式:
- 多进程模式;
- 多线程模式;
- 多进程+多线程模式。
多进程
os模块进程方法
- os.getppid():获取父进程id;
- os.getpid():获取当前进程id;
- os.fork():创建子进程(仅支持Unix/Linux/Mac);
fork进程
在Python的os模块的fork函数可以轻松创建子进程,但是fork()仅支持Unix/Linux/Mac;
调用fork函数创建进程,需要注意fork()调用一次,返回两次:
for()调用时操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回;
- 子进程永远返回0;
- 父进程返回子进程的ID。
1 | import os |
跨平台进程模块
multiprocessing模块就是跨平台版本的多进程模块.
multiprocessing模块提供了一个Process类来表示一个进程对象;
Process进程对象
构造方法
1 | def __init__(self, group=None, target=None, name=None, args=(), kwargs={}) |
实例方法
1 | is_alive():返回进程是否在运行 |
实例属性
1 | - authkey:进程的认证密钥(字节字符串) |
示例
1 | from multiprocessing import Process |
Pool进程池
如果要启动大量的子进程,可以用进程池Pool的方式批量创建子进程;
要点
- Pool的默认大小是CPU的核数;
Pool实例方法
- 创建进程池:Pool(count);
- 创建子进程:
1 | # 创建子进程任务,串行执行(需要等待当前子进程执行完毕后,再执行下一个进程); |
- close():关闭pool,使其不在接受新的任务;
- terminate():结束工作进程,不在处理未完成的任务。
- join():主进程阻塞,等待子进程的退出,join方法要在close或terminate之后使用。
示例
1 | from multiprocessing import Pool |
subprocess子进程模块
subprocess模块用来创建子进程;
subprocess模块允许创建一个新的进程来执行另外的程序,可以与进程进行通信,获取标准的输入、标准输出、标准错误以及返回码等。
subprocess模块常用函数
- 常用函数表:
函数 | 描述 |
---|---|
run() | Python 3.5中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。 |
call() | 执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。 |
check_call() | Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。 |
check_output() | Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。 |
getoutput(cmd) | 接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。 |
getstatusoutput(cmd) | 执行cmd命令,返回一个元组(命令执行状态, 命令执行结果输出),其功能类似于commands.getstatusoutput()。 |
表中常用函数都是基于subprocess.Popen类实现的。
1 | 【说明】 |
- 常用函数的定义:
1 | subprocess.call(*popenargs, *, stdin=None, stdout=None, stderr=None, shell=False, timeout=None) |
1 | 【参数说明】 |
- 示例:
1 | import subprocess |
subproces.Popen()
如果我们需要更复杂功能时,并且要与进程进行复杂的交互,可以利用Popen类来实现;
1> 构造函数概述:
1 | # 构造函数 |
注意:如果希望通过进程的stdin向其发送数据,在创建Popen对象的时候,参数stdin必须被设置为PIPE。同样,如果希望从stdout和stderr获取数据,必须将stdout和stderr设置为PIPE。
2> Popen实例方法:
方法 | 描述 |
---|---|
poll() | 用于检查子进程(命令)是否已经执行结束,没结束返回None,结束后返回状态码。 |
wait(timeout=None) | 等待子进程结束,并返回状态码;如果在timeout指定的秒数之后进程还没有结束,将会抛出一个TimeoutExpired异常。 |
communicate() | 和子进程交互(发送数据到stdin,并从stdout和stderr读数据,直到收到EOF,要给子进程的stdin发送数据,则Popen时要设置stdin要为PIPE)。等待子进程结束。 |
send_signal(signal) | 发送指定的信号给这个子进程。 |
terminate() | 停止该子进程。 |
kill() | 杀死该子进程。 |
3> Popen实例属性:
1 | > p.pid: 子进程的PID。 |
4> 示例:
1 | import subprocess |
进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。
Queue
在python中,多个线程之间的数据是共享的,多个线程进行数据交换的时候,不能够保证数据的安全性和一致性,所以当多个线程需要进行数据交换的时候,队列就出现了,队列可以完美解决线程间的数据交换,保证线程间数据的安全性和一致性。
三种队列:
- Python queue模块的FIFO队列先进先出。 class queue.Queue(maxsize)
- LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize)
- 还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)
常用方法:
- queue.qsize() 返回队列的大小
- queue.empty() 如果队列为空,返回True,反之False
- queue.full() 如果队列满了,返回True,反之False
- queue.full 与 maxsize 大小对应
- queue.get([block[, timeout]])获取队列,timeout等待时间(从队列中移除并返回一个数据,通知q可以继续添加数据)
- queue.get_nowait() 相当queue.get(False)
- queue.put(item) 写入队列,timeout等待时间(默认的情况,阻塞调用,等待q.get移除数据后继续执行)
- queue.put_nowait(item) 相当queue.put(item, False)
- queue.task_done() 在完成一项工作之后,queue.task_done()函数向任务已经完成的队列发送一个信号
- queue.join() 实际上意味着等到队列为空,再执行别的操作
示例:
在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
1 | from multiprocessing import Process, Queue |
多进程的优缺点
优点
- 多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
缺点
- 多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程
threading模块
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
线程概念
- 由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程;
- Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。
- 主线程实例的名字叫MainThread,子线程的名字在创建时指定,如果不起名字Python就自动给线程命名为Thread-1,Thread-2等。
缺点
- 多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
创建线程实例
启动一个线程:创建Thread实例,需要传入一个处理函数传入,然后调用start()开始执行;
1 | import time, threading |
Lock(锁)
锁用来解决多个线程间数据共享导致的数据修改错误;
数据共享问题
- 多进程中,同一个变量,互不影响;
- 多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
使用锁
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
- 多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
- 获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try…finally来确保锁一定会被释放。
- 锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
示例
1 | import time, threading |
GIL全局锁
1.现象描述?
理论上来看一个死循环线程会100%占用一个CPU,用C、C++或Java在n个线程中写相同的死循环,直接可以把n核心跑满,4核就跑到400%,8核就跑到800%,但是Python不会占满n核CPU;
2.python的GIL锁;
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
3.利用多核CPU:
- 通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
- 在Python中,可以使用多线程,但不要指望能有效利用多核。
- 如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁,但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,ThreadLocal类型的全局变量,可以绑定其他变量,并且可以在多个线程中使用它,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
- 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
- ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
示例
- 创建ThreadLocal全局变量,通过绑定其它变量可以在多个线程中互不影响的特性,解决参数在一个线程中各个函数之间互相传递的问题;
1 | import threading |
分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
例子
需要在linux/mac系统下,windows无法正常运行;
如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。
- 我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:
1 | # task_master.py |
1 | 请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。 |
- 然后,在另一台机器上启动任务进程(本机上启动也可以):
1 | # task_worker.py |
- 运行
1 | 先启动task_master.py服务进程: |
常用内建模块
datetime
datetime是Python处理日期和时间的标准库,主要使用datetime模块中的datatime类来获取时间。
1 | # 导入datetime类 |
本地时间
1 | from datetime import datetime |
时间加减法
timedelta对象表示一个时间长度,用来计算两个日期或者时间的差值;
1 | from datetime import datetime, timedelta |
时区时间
UTC时间是格林威治标准时间,比北京时间早了8小时。
创建时区时间
- 本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。
- datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区;
1 | from datetime import datetime, timedelta, timezone |
时区转换
可以先通过utcnow()拿到当前的UTC时间,再通过astimezone根据时间差转换为任意时区的时间;
1 | from datetime import datetime, timedelta, timezone |
时间戳
Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。
1 | from datetime import datetime |
时间格式化
Python time strftime() 函数接收以时间元组,并返回以可读字符串表示的当地时间,格式由参数format决定。
语法
1 | time.strftime(format[, t]) |
时间日期格式化符号
python中时间日期格式化符号:
1 | %y 两位数的年份表示(00-99) |
示例
1 | from datetime import datetime |
collections
collections是Python内建的一个集合模块,提供了许多有用的集合类。
namedtuple
利用元组创建对象;
namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。
语法
1 | namedtuple('名称', [属性list]): |
示例
1 | from collections import namedtuple |
deque
加强版list;
使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:
- deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。
1 | from collections import deque |
defaultdict
dict设置默认值;
使用dict时,当通过dict.[key]访问value时,如果引用的Key不存在,就会报错KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict;
- 注意默认值是调用函数返回的,而函数在创建defaultdict对象时传入。
- 除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的。
1 | from collections import defaultdict |
OrderedDict
dict的key支持按顺序迭代;
使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。如果要保持Key的顺序,可以用OrderedDict;
- OrderedDict的Key会按照插入的顺序排列,不是Key本身排序;
1 | from collections import OrderedDict |
实现FIFO的dict
OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key;
1 | from collections import OrderedDict |
ChainMap
让参数按定义的顺序查找;
ChainMap可以把一组dict串起来组成一个逻辑上的dict。ChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。
使用场景示例
什么时候使用ChainMap最合适?举个例子:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。
1 | from collections import ChainMap |
Counter
Counter是一个简单的计数器,Counter实际上也是dict的一个子类;
示例:统计字符出现的个数?
1 | from collections import Counter |
base64
Base64是一种用64个字符来表示任意二进制数据的方法,Base64是一种最常见的二进制编码方法。
base64编解码
1 | import base64 |
urlsafe_b64编解码
urlsafe_b64编码后把字符+和/分别变成-和_;
1 | import base64 |
要点
- Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
- Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
- 由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉;
- Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了;
例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32#【解码byte非4的倍数用等号补位】
import base64
# 能处理掉“=”的base64解码函数
def safe_base64_decode(s):
# 添加等于号
if len(s) % 4 != 0:
s = s + bytes('=', encoding='utf-8') * (4 - len(s) % 4)
print("1:%s" % s)
# 解决字符串和bytes类型
if not isinstance(s, bytes):
s = bytes(s, encoding='utf-8')
print('s:%s' % s)
# 解码
base64_string = base64.b64decode(s)
print("base64:%s" % base64_string)
return base64_string
# 测试:
# assert b'abcd' == safe_base64_decode(b'YWJjZA=='), safe_base64_decode('YWJjZA==')
# assert b'abcd' == safe_base64_decode(b'YWJjZA'), safe_base64_decode('YWJjZA')
# print('ok')
# 解码带等号=的bytes
safe_base64_decode(b'YWJjZA==')
# 字符串转byte再解码
safe_base64_decode('YWJjZA==')
# 编码长度不是4的倍数时,用等号补位;
safe_base64_decode(b'YWJjZA')
struct
Python提供了一个struct模块来解决bytes和其它数据类型的转换;
根据格式化字符串(参考struct官方文档)的格式来进行bytes和其它数据类型的转换;
主要函数
struct模块中最主要的三个函数式pack()、unpack()、calcsize(),语法如下:
1 | pack(fmt, v1, v2, ...) ------ 根据所给的fmt指令描述的格式将值v1,v2,...转换为一个字符串,后面参数个数要和处理指令一致。 |
fmt由字节顺序符和格式字符组成(参考struct官方文档);
示例
1 | import struct |
hashlib
Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。
摘要算法
任何摘要算法都是把无限多的数据集合映射到一个有限的集合中, 这种碰撞有可能逆推出原文;
- 摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
- 摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。
- 摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
- 摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
MD5
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
1 | import hashlib |
SHA1
- SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
- 比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
1 | import hashlib |
hmac
Python内置的hmac模块实现了标准的Hmac算法,它利用一个key对message计算“杂凑”后的hash,使用hmac算法比标准hash算法更安全,因为针对相同的message,不同的key会产生不同的hash。
解决问题场景
问题
通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5对比计算md5(password)的结果,如果一致,用户输入的口令就是正确的。
解决思路
为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能仅针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希,这样,大大增加了黑客破解的难度。
hmac算法
和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。
1 | import hmac |
可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes。
itertools
- Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
- itertools模块提供的全部是处理迭代功能的函数,它们的返回值不是list,而是Iterator,只有用for循环迭代的时候才真正计算。
无限迭代函数
1 | import itertools,time |
多个迭代串联
chain()可以把一组迭代对象串联起来,形成一个更大的迭代器:
1 | import itertools,time |
重复元素分组迭代
groupby()把迭代器中相邻的重复元素挑出来放在一起:
1 | import itertools,time |
contextlib
在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。可以使用try…finally,或者with;
with自动关闭资源
并不是只有open()函数返回的fp对象才能使用with语句。实际上,任何对象,只要正确实现了上下文管理,就可以用于with语句。
1 | 实现上下文管理是通过__enter__和__exit__这两个方法实现的。 |
1 | class Query(object): |
@contextmanager
上下文管理装饰器
@contextmanager让我们通过编写generator来简化上下文管理。
实现with
- @contextmanager这个decorator接受一个generator,用yield语句把with … as var把变量输出出去,然后,with语句就可以正常地工作了:
1 | from contextlib import contextmanager |
- 希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现:
1 | from contextlib import contextmanager |
@closing
如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。
@closing的实现
closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:
1 |
|
使用案例
closing的作用就是把任意对象变为上下文对象,并支持with语句。
1 | from contextlib import closing |
urllib
urllib提供了一系列用于操作URL的功能。
urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标识浏览器的。
Get
抓取Get请求的信息
urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应;
例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取,可以看到HTTP响应的头和JSON数据:
1 | from urllib import request |
模拟浏览器发送GET请求
如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。
例如,模拟iPhone 6去请求豆瓣首页,这样豆瓣会返回适合iPhone的移动版网页:
1 | from urllib import request |
Post
如果要以POST发送一个请求,只需要把参数data以bytes形式传入。
案例:模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入;
1 | print('Login to weibo.cn...') |
Handler
如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:
1 | from urllib import request |
XML
操作XML有两种方法:DOM和SAX。
- DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。
- SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
SAX解析XML
- SAX解析的关键函数:from xml.parsers.expat import ParserCreate;
- 在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element,end_element和char_data,准备好这3个函数,然后就可以解析xml了。
解析步骤
例如:解析一个html节点
1 | <a href="/">python</a> |
- 会产生3个事件:
1 | start_element事件,在读取<a href="/">时; |
- 定义实现了1中三个事件方法的类;
- 通过指定ParserCreate实例的三个属性与1中三个方法;
- 最后调用ParserCreate实例对象的Parse(xml)函数来解析;
案例
1 | from xml.parsers.expat import ParserCreate |
生成XML
除了解析XML外,如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串;
1 | L = [] |
HTMLParser
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML,Python提供了HTMLParser来非常方便地解析HTML。
- feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
- 特殊字符有两种,一种是英文表示的 ,一种是数字表示的Ӓ,这两种字符都可以通过Parser解析出来。
1 | from html.parser import HTMLParser |
常用第三方模块
除了内建的模块外,Python还有大量的第三方模块,所有的第三方模块都会在pypi上注册,只要找到对应的模块名字,即可用pip安装。
Pillow(图像处理)
Pillow是图像处理标准库,支持最新Python 3.x,详情见API文档;
安装
1 | $ pip install pillow |
操作图像
操作图像的缩放、切片、旋转、滤镜、输出文字、调色板等功能。
缩放
- 打开一张图片并进行缩放:
1 | from PIL import Image |
模糊
- ImageFilter类中预定义了如下滤波方法:
1 | • BLUR:模糊滤波 |
- 示例:
1 | from PIL import Image, ImageFilter |
绘图
PIL的ImageDraw提供了一系列绘图方法,让我们可以直接绘图。
生成字母验证码图片
用随机颜色填充背景,再画上文字,最后对图像进行模糊,得到验证码图片:
1 | from PIL import Image, ImageDraw, ImageFont, ImageFilter |
requests(处理url)
处理URL资源比urllib更方便;
安装
1 | $ pip install requests |
GET请求
使用GET请求
1 | import requests |
GET请求带参数
1 | import requests |
响应数据格式Json
1 | import requests |
请求头添加header
1 | import requests |
POST请求
POST请求和GET请求用法类似,只需要把get()方法变成post(),然后传入data参数作为POST请求的数据;
请求参数
- requests默认使用application/x-www-form-urlencoded对POST数据编码。
1 | import requests |
- 如果要传递JSON数据,可以直接传入json关键字参数,内部自动序列化为JSON;
1 | import requests |
- 上传文件:上传文件需要更复杂的编码格式,但是requests把它简化成files参数;
1 | import requests |
chardet(编码检测)
用它来检测编码类型;
使用chardet检测编码非常容易,chardet支持检测中文、日文、韩文等多种语言。详情参考官方API文档
安装
1 | $ pip install chardet |
常用编码检查
1 | import chardet |
psutil(系统信息)
在Python中获取系统信息可以使用subprocess模块,另一个更简单的方法是使用psutil这个第三方模块。
psutil还可以获取用户信息、Windows服务等很多有用的系统信息,具体请参考psutil的官网:https://github.com/giampaolo/psutil;
安装
1 | $ pip install psutil |
获取CPU信息
1 | import psutil |
获取内存信息
1 | import psutil |
获取磁盘信息
可以通过psutil获取磁盘分区、磁盘使用率和磁盘IO信息:
1 | import psutil |
获取网络信息
psutil可以获取网络接口和网络连接信息;
1 | import psutil |
获取进程信息
- psutil让Python程序获取系统信息;
- psutil还可以获取用户信息、Windows服务等很多有用的系统信息,具体请参考psutil的官网:https://github.com/giampaolo/psutil;
- 常用操作方法:
1 | import psutil |
- Linux ps命令用于显示当前进程 (process) 的状态,psutil还提供了一个test()函数,可以模拟出ps命令的效果。
1 | print(psutil.test()) |
独立的python运行环境
virtualenv为python应用提供了隔离的Python运行环境,解决了不同应用间使用相同第三方或者外部模块引起多版本的冲突问题。
需要注意的是,pycharm创建的项目默认安装了virtualenv,并且pycharm terminal默认会进入venv环境。
安装virtualenv
1 | $ pip3 install virtualenv |
venv环境
venv环境是python项目根目录下的venv文件夹,通过命令virtualenv —no-site-packages可以让venv文件夹变为一个不带任何第三方包的“干净”的Python运行环境;
- 创建项目;
- 创建并进入venv环境,创建好venv环境后,命令行前面会有(venv)前缀,如下:
1 | D:\pytest> $ virtualenv --no-site-packages venv |
venv环境指定项目的python版本
1 | $ virtualenv -p /usr/bin/python2.7 my_project |
- 项目venv环境中安装第三方包:
1 | $ pip install chardet |
- 退出venv环境:
1 | $ deactivate |
- 进入venv环境:
1 | //linux |
图形界面
Python支持多种图形界面的第三方库,包括:
- Tk(Python自带的库Tkinter是支持Tk的)
- wxWidgets
- Qt
- GTK
Tkinter
Python内置的Tkinter可以满足基本的GUI程序的要求,如果是非常复杂的GUI程序,建议用操作系统原生支持的语言和库来编写.
- Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;
- Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。
- Tkinter封装了访问Tk的接口,我们的代码只需要调用Tkinter提供的接口就可以了;
GUI API
Frame
Frame是所有Widget的父容器,Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。
GUI案例
显示文本
1 | from tkinter import * |
输入和弹窗
1 | from tkinter import * |
turtle
Python内置了turtle库,基本上100%复制了原始的Turtle Graphics(海龟绘图)的所有功能。
turtle可以执行一个绘制图形的过程,绘制出各种复杂的图形;
案例
绘制一个长方形
1 | # 导入turtle包的所有内容: |
循环绘制5个五角星
1 | from turtle import * |
绘制一棵分型树
1 | from turtle import * |
网络编程
用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。
TCP/IP
互联网的协议简称TCP/IP协议.
IP协议
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
IPv4
IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6
IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334.
TCP协议
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
三次握手
- 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
TCP协议栈的弱点
TCP连接的资源消耗,其中包括:数据包信息、条件状态、序列号等。通过故意不完成建立连接所需要的三次握手过程,造成连接一方的资源耗尽。
端口
80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。
TCP编程
socket
- Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
- 大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
- 用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。
- 同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
socket使用步骤示例
1 | # 导入socket库: |
客户端
客户端使用socket请求服务端并获得响应;
大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
服务器
和客户端编程相比,服务器编程就要复杂一些。
服务端处理思路
- 服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
- 服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
- 但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。
CS通信示例
创建一个服务端,用来接收到用户的链接,当接收到用户输入的信息时,都将接收的内容加上“我不明白”再发回去,用户输入exit时,客户端退出socket连接。
实现步骤如下:
- 服务端代码:
1 | import socket |
- 客户端代码:
1 | import socket |
UDP编程
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。
- 使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。
- 虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
UDP的使用与TCP类似,但是不需要建立连接。此外,客户端不需要调用connect建立连接和服务端不需要调用listen,并且服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。
示例
服务端和客户端使用UDP通信,服务端接收客户端信息,加上hello后,再返回给客户端;
- 服务端代码:
1 | import socket |
- 客户端代码:
1 | import socket |
实现收发电子邮件
名词解释
- MUA:Mail User Agent——邮件用户代理。
- MTA:Mail Transfer Agent——邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。
- MDA:Mail Delivery Agent——邮件投递代理。
收发邮件
流程
1 | 发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人 |
要编写程序来发送和接收邮件,本质上就是:
- 编写MUA把邮件发到MTA;
- 编写MUA从MDA上收邮件;
协议
发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。
收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。
SMTP服务器配置
邮件客户端软件在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。假设你正在使用163的邮箱,你就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,你得填163提供的SMTP服务器地址:smtp.163.com,为了证明你是163的用户,SMTP服务器还要求你填写邮箱地址和邮箱口令,这样,MUA才能正常地把Email通过SMTP协议发送到MTA。
类似的,从MDA收邮件时,MDA服务器也要求验证你的邮箱口令,确保不会有人冒充你收取你的邮件,所以,Outlook之类的邮件客户端会要求你填写POP3或IMAP服务器地址、邮箱地址和口令,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。
特别注意:代理客户端收发邮件,需要本地配置邮件的登录信息,并且,目前大多数邮件服务商都需要手动打开SMTP发信和POP收信的功能,否则只允许在网页登录;
SMTP发送邮件
SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。
Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
发文本邮件
- 构造邮件内容:
1 | from email.mime.text import MIMEText |
- 邮件主题、发件人、收件人信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,如果没有,可能无法发送;
- 主要代码如下:
1 | from email.mime.text import MIMEText |
发送附件
带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase附件对象即可;
1 | from email import encoders |
发送图片
邮件服务商一般自动屏蔽邮件正文的图片,如果将图片添加邮件html正文,需要先上传图片,然后按照顺序编号cid:x引用图片即可;
在上传附件的代码后添加如下代码:
1 | msg.attach(MIMEText('<html><body><h1>Hello</h1>' + |
同时支持HTML和Plain格式
如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了HTML邮件怎么办?
办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。
利用MIMEMultipart就可以组合一个HTML和Plain,要注意指定subtype是alternative:
1 | msg = MIMEMultipart('alternative') |
加密SMTP
使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。
只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全连接。
示例
某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。
必须知道,Gmail的SMTP端口是587,因此,修改代码如下:
1 | smtp_server = 'smtp.gmail.com' |
SMTP发件总结
使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。
构造一个邮件对象就是一个Messag对象,如果构造一个MIMEText对象,就表示一个文本邮件对象,如果构造一个MIMEImage对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象。它们的继承关系如下:
1 | Message |
这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。
POP3收取邮件
收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。
Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。
收邮件
注意到POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。
收取邮件分两步:
- 用poplib把邮件的原始文本下载到本地;
- 用email解析原始文本,还原为邮件对象。
用Python的poplib模块收取邮件分两步:第一步是用POP3协议把邮件获取到本地,第二步是用email模块把原始邮件解析为Message对象,然后,用适当的形式把邮件内容展示给用户即可。
通过POP3获取邮件
POP3协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:
用POP3获取邮件其实很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。
如下获取邮件的代码(包含删除邮件):
1 | import poplib |
解析邮件
解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块,只需要一行代码就可以把邮件内容解析为Message对象:
1 | from email.parser import Parser |
但是这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。
所以我们要递归地打印出邮件Message对象的层次结构:
1 | # 解码邮件内容 |
数据的存储
定义数据的存储格式,主要有以下方式:
- 文本文件存储:每条数据一行,字段用逗号隔开;
- json文本文件存储:数据以json的格式保存本地;
- SQLite
- MySQL
SQLite
SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
使用SQLite
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。
- 在Python中操作数据库时,要先导入数据库对应的驱动,然后,通过Connection对象和Cursor对象操作数据。
- 要确保打开的Connection对象和Cursor对象都正确地被关闭,否则,资源就会泄露。
- 使用try:…except:…finally:…确保关闭掉Connection对象和Cursor对象;
- 必须提交事务,修改的操作才会生效;
- SQLite的sql中的参数占位符是问号?;
- python操作数据库查询表的结果是list,list的每一个元素是tuple;
示例代码如下:
1 | # 导入SQLite驱动: |
如果SQL语句带有参数,那么需要把参数按照位置传递给execute()方法,有几个?占位符就必须对应几个参数,例如:
1 | cursor.execute('select * from user where name=? and pwd=?', ('abc', 'password')) |
MySQL
安装MySQL驱动
1 | //mysql官方驱动 |
操作MySQL
- 执行INSERT等操作后要调用commit()提交事务;
- 操作MySQL同操作Sqlite相同,但是MySQL的SQL中的参数占位符是%s。
示例代码如下:
1 | # 1. 导入MySQL驱动: |
ORM
ORM即Object-Relational Mapping,把关系数据库的表结构映射到对象上。ORM框架的作用就是把数据库表的一行记录与一个对象互相做自动转换。
SQLAlchemy
在Python中,最有名的ORM框架是SQLAlchemy。
安装
1 | $ pip install sqlalchemy |
使用示例
1 | # 1. 导入: |
web开发
HTTP状态码
响应代码:
- 200表示成功;
- 3xx表示重定向;
- 4xx表示客户端发送的请求有错误;
- 5xx表示服务器端处理时发生了错误;
HTTP格式
每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。
Body:
- Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
- 当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。
WSGI
WSGI(Web Server Gateway Interface),WSGI接口定义非常简单,它只要求Web开发者实现一个函数,WSGI的处理函数,针对每个HTTP请求进行响应;
Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。
- 无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。
- 复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化Web开发。
http请求信息
1 | //请求方式:GET/POST/PUT/DELETE |
根据请求响应
如果通过判断请求信息来响应结果,项目将难以维护,此时就需要web框架了;
1 | def application(environ, start_response): |
示例
如下,hello.py文件中定义用来响应客户端请求的函数,server.py利用wsgiref定义一个服务监听请求,响应application函数返回的结果;
1 | # hello.py |
1 | # server.py |
web框架
- Flask: 轻量级 Web 应用框架(hot);
- Django:全能型Web框架;
- web.py:一个小巧的Web框架;
- Bottle:和Flask类似的Web框架;
- Tornado:Facebook的开源异步Web框架。
Flask
Flask 是Python比较流行的Web框架;
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2。Flask没有默认使用的数据库、窗体验证工具。用 extension增加其他功能。
然而,Flask保留了扩增的弹性,可以用Flask-extension加入这些功能:ORM、窗体验证工具、文件上传、各种开放式身份验证技术。此文章时的最新版本为1.0.2。
安装Flask
1 | pip install -U Flask |
WebAPI
核心概念
- Flask通过Python的装饰器在内部自动地把URL和处理函数给关联起来;
- Flask自带的Server在端口5000上监听;
- Flask通过request.form[‘name’]来获取表单的内容。
示例
Flask框架,编写接口来支持GET/POST请求返回html;
1 | from flask import Flask |
jinja2模板技术
模板技术,我们以MVC模式进行开发:
- Controller层:Controller负责业务逻辑,即处理请求URL的函数;
- View层:HTML文档作为view层的UI模板,嵌入了一些变量 和指令,根据我们动态传入的数据,替换后,得到最终的HTML,发送给用户;
- Model层:是用来传给View的数据,通常dict作为Model;
通过MVC,我们在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样,我们就成功地把Python代码和HTML代码最大限度地分离了。
jinja2
- Flask通过render_template()函数来实现模板的渲染,具体html中支持模板渲染需要是jinja2。
- 和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2,所以我们先直接安装jinja2:
1 | pip install jinja2 |
jinja2-API
在Jinja2模板中:
- 模板文件:一定要把html模板放到templates目录下,templates和server.py服务文件在同级目录下;
1 | project |
- 变量:用表示一个需要替换的变量。
- 语法指令:很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用
1 | {% ... %} |
表示指令,逻辑判断的语法就写在指令中。
1 | <!--比如循环输出的代码--> |
示例
使用mvc模式简单的实现一个用户登录的功能!
1.编写模板(templates):
- 首页模板home.html:
1 | <!--用来显示首页的模板--> |
- 登录页面模板from.html:
1 | <!--用来显示登录表单的模板--> |
- 登录成功页面模板signin-ok.html:
1 | <!--登录成功的模板:--> |
2.编写Controller/Model层:server.py
1 | from flask import Flask, request, render_template |
其它模板框架
除了Jinja2,常见的模板还有:
1 | - Mako:用<% ... %>和${xxx}的一个模板; |
异步IO
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
解决IO阻塞的方法:
- 多线程;
- 异步IO;
多线程解决IO阻塞
解决方式
因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。
性能问题
多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。
异步IO解决IO阻塞
解决IO阻塞问题的另一种解决方法是异步IO。
在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
协程
协程,又称微线程,纤程。英文名coroutine(协同程序)。即多个程序系统完成工作的过程;
概念
子程序
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,子程序调用总是一个入口,一次返回,调用顺序是明确的。一个线程就是执行一个子程序。
协程和子程序
而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
特点优势
- 协程可以看成子程序的切换,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,因此协程有着极高的执行效率。和多线程比,线程数量越多,协程的性能优势就越明显。
- 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
- 协程是一个线程执行,利用多核CPU最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
协程的实现
Python对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。
示例解释协程
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高;
1 | import time |
注意到consumer函数是一个generator,把一个consumer传入produce后:
- 首先调用c.send(None)启动生成器;
- 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
- consumer通过yield拿到消息,处理,又通过yield把结果传回;
- produce拿到consumer处理的结果,继续生产下一条消息;
- produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
asyncio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持,asyncio提供了完善的异步IO支持;
asyncio可以实现单线程并发IO操作.
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
asyncio使用步骤
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from异步调用另一个coroutine实现异步操作。
- @asyncio.coroutine把一个generator函数逻辑标记为coroutine类型,处理函数的内部异步操作需要在coroutine中通过yield from完成;然后,我们就把这个coroutine扔到EventLoop中执行。
- yield from语法可以方便地异步调用另一个耗时逻辑的coroutine;
- coroutine类型函数的多次调用,可以封装成一组Task然后并发执行。
- EventLoop等待asyncio.wait(tasks)并发执行多个任务;
- 关闭EventLoop;
asyncio并发示例
示例1:实现并发执行两个耗时逻辑!
1 | import threading |
两个coroutine是由同一个线程并发执行的。asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。
示例2:用asyncio的异步网络连接来获取sina、sohu和163的网站首页!
1 | import asyncio |
async/await
新异步IO语法;
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让异步执行coroutine的代码更简洁易读。
使用
请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要对asyncio做两步简单的替换:
- 把@asyncio.coroutine替换为async;
- 把yield from替换为await。
示例代码
并发执行异步任务打印hello!
1 | import asyncio |
aiohttp
服务器端实现单线程+coroutine并发IO操作;
asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。
asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。
安装aiohttp
1 | pip install aiohttp |
使用
创建aiohttp的初始化函数init(),init()也是一个coroutine,在函数中loop.create_server()则利用asyncio创建TCP服务。
示例
编写一个HTTP服务器,分别处理以下URL:
1 | / - 首页返回b'<h1>Index</h1>'; |
代码实现:
1 | import asyncio |
或者用loop:
1 | import asyncio |